/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts.common.list;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.SearchSnippets;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.telephony.PhoneNumberUtils;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView.SelectionBoundsAdjuster;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.QuickContactBadge;
import android.widget.TextView;
import com.android.contacts.common.ContactPresenceIconUtil;
import com.android.contacts.common.ContactStatusUtil;
import com.android.contacts.common.R;
import com.android.contacts.common.format.TextHighlighter;
import com.android.contacts.common.list.PhoneNumberListAdapter.Listener;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.contacts.common.util.SearchUtil;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.util.ViewUtil;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A custom view for an item in the contact list. The view contains the contact's photo, a set of
 * text views (for name, status, etc...) and icons for presence and call. The view uses no XML file
 * for layout and all the measurements and layouts are done in the onMeasure and onLayout methods.
 *
 * <p>The layout puts the contact's photo on the right side of the view, the call icon (if present)
 * to the left of the photo, the text lines are aligned to the left and the presence icon (if
 * present) is set to the left of the status line.
 *
 * <p>The layout also supports a header (used as a header of a group of contacts) that is above the
 * contact's data and a divider between contact view.
 */
public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster {

  /** IntDef for indices of ViewPager tabs. */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({NONE, VIDEO, DUO, CALL_AND_SHARE})
  public @interface CallToAction {}

  public static final int NONE = 0;
  public static final int VIDEO = 1;
  public static final int DUO = 2;
  public static final int CALL_AND_SHARE = 3;

  private final PhotoPosition mPhotoPosition = getDefaultPhotoPosition();
  private static final Pattern SPLIT_PATTERN =
      Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
  static final char SNIPPET_START_MATCH = '[';
  static final char SNIPPET_END_MATCH = ']';
  /** A helper used to highlight a prefix in a text field. */
  private final TextHighlighter mTextHighlighter;
  // Style values for layout and appearance
  // The initialized values are defaults if none is provided through xml.
  private int mPreferredHeight = 0;
  private int mGapBetweenImageAndText = 0;
  private int mGapBetweenLabelAndData = 0;
  private int mPresenceIconMargin = 4;
  private int mPresenceIconSize = 16;
  private int mTextIndent = 0;
  private int mTextOffsetTop;
  private int mNameTextViewTextSize;
  private int mHeaderWidth;
  private Drawable mActivatedBackgroundDrawable;
  private int mCallToActionSize = 48;
  private int mCallToActionMargin = 16;
  // Set in onLayout. Represent left and right position of the View on the screen.
  private int mLeftOffset;
  private int mRightOffset;
  /** Used with {@link #mLabelView}, specifying the width ratio between label and data. */
  private int mLabelViewWidthWeight = 3;
  /** Used with {@link #mDataView}, specifying the width ratio between label and data. */
  private int mDataViewWidthWeight = 5;

  private ArrayList<HighlightSequence> mNameHighlightSequence;
  private ArrayList<HighlightSequence> mNumberHighlightSequence;
  // Highlighting prefix for names.
  private String mHighlightedPrefix;
  /** Indicates whether the view should leave room for the "video call" icon. */
  private boolean mSupportVideoCall;

  // Header layout data
  private TextView mHeaderTextView;
  private boolean mIsSectionHeaderEnabled;
  // The views inside the contact view
  private boolean mQuickContactEnabled = true;
  private QuickContactBadge mQuickContact;
  private ImageView mPhotoView;
  private TextView mNameTextView;
  private TextView mLabelView;
  private TextView mDataView;
  private TextView mSnippetView;
  private TextView mStatusView;
  private ImageView mPresenceIcon;
  @NonNull private final ImageView mCallToActionView;
  private ImageView mWorkProfileIcon;
  private ColorStateList mSecondaryTextColor;
  private int mDefaultPhotoViewSize = 0;
  /**
   * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
   * to align other data in this View.
   */
  private int mPhotoViewWidth;
  /**
   * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
   */
  private int mPhotoViewHeight;
  /**
   * Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of
   * the photo should have horizontal padding on those left assuming there is a photo.
   */
  private boolean mKeepHorizontalPaddingForPhotoView;
  /** Only effective when {@link #mPhotoView} is null. */
  private boolean mKeepVerticalPaddingForPhotoView;
  /**
   * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
   * False indicates those values should be updated before being used in position calculation.
   */
  private boolean mPhotoViewWidthAndHeightAreReady = false;

  private int mNameTextViewHeight;
  private int mNameTextViewTextColor = Color.BLACK;
  private int mPhoneticNameTextViewHeight;
  private int mLabelViewHeight;
  private int mDataViewHeight;
  private int mSnippetTextViewHeight;
  private int mStatusTextViewHeight;
  private int mCheckBoxWidth;
  // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
  // same row.
  private int mLabelAndDataViewMaxHeight;
  private boolean mActivatedStateSupported;
  private boolean mAdjustSelectionBoundsEnabled = true;
  private Rect mBoundsWithoutHeader = new Rect();
  private CharSequence mUnknownNameText;

  private String mPhoneNumber;
  private int mPosition = -1;
  private @CallToAction int mCallToAction = NONE;

  public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
    this(context, attrs);

    mSupportVideoCall = supportVideoCallIcon;
  }

  public ContactListItemView(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a;

    if (R.styleable.ContactListItemView != null) {
      // Read all style values
      a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
      mPreferredHeight =
          a.getDimensionPixelSize(
              R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
      mActivatedBackgroundDrawable =
          a.getDrawable(R.styleable.ContactListItemView_activated_background);
      mGapBetweenImageAndText =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
              mGapBetweenImageAndText);
      mGapBetweenLabelAndData =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
              mGapBetweenLabelAndData);
      mPresenceIconMargin =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin);
      mPresenceIconSize =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
      mDefaultPhotoViewSize =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
      mTextIndent =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
      mTextOffsetTop =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
      mDataViewWidthWeight =
          a.getInteger(
              R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight);
      mLabelViewWidthWeight =
          a.getInteger(
              R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight);
      mNameTextViewTextColor =
          a.getColor(
              R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor);
      mNameTextViewTextSize =
          (int)
              a.getDimension(
                  R.styleable.ContactListItemView_list_item_name_text_size,
                  (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
      mCallToActionSize =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_video_call_icon_size, mCallToActionSize);
      mCallToActionMargin =
          a.getDimensionPixelOffset(
              R.styleable.ContactListItemView_list_item_video_call_icon_margin,
              mCallToActionMargin);

      setPaddingRelative(
          a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0),
          a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0),
          a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0),
          a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0));

      a.recycle();
    }

    mTextHighlighter = new TextHighlighter(Typeface.BOLD);

    if (R.styleable.Theme != null) {
      a = getContext().obtainStyledAttributes(R.styleable.Theme);
      mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
      a.recycle();
    }

    mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);

    if (mActivatedBackgroundDrawable != null) {
      mActivatedBackgroundDrawable.setCallback(this);
    }

    mNameHighlightSequence = new ArrayList<>();
    mNumberHighlightSequence = new ArrayList<>();

    mCallToActionView = new ImageView(getContext());
    mCallToActionView.setId(R.id.call_to_action);
    mCallToActionView.setLayoutParams(new LayoutParams(mCallToActionSize, mCallToActionSize));
    mCallToActionView.setScaleType(ScaleType.CENTER);
    mCallToActionView.setImageTintList(
        ContextCompat.getColorStateList(getContext(), R.color.search_video_call_icon_tint));
    addView(mCallToActionView);

    setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
  }

  public static PhotoPosition getDefaultPhotoPosition() {
    int layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
    return layoutDirection == View.LAYOUT_DIRECTION_RTL ? PhotoPosition.RIGHT : PhotoPosition.LEFT;
  }

  /**
   * Helper method for splitting a string into tokens. The lists passed in are populated with the
   * tokens and offsets into the content of each token. The tokenization function parses e-mail
   * addresses as a single token; otherwise it splits on any non-alphanumeric character.
   *
   * @param content Content to split.
   * @return List of token strings.
   */
  private static List<String> split(String content) {
    final Matcher matcher = SPLIT_PATTERN.matcher(content);
    final ArrayList<String> tokens = new ArrayList<>();
    while (matcher.find()) {
      tokens.add(matcher.group());
    }
    return tokens;
  }

  public void setUnknownNameText(CharSequence unknownNameText) {
    mUnknownNameText = unknownNameText;
  }

  public void setQuickContactEnabled(boolean flag) {
    mQuickContactEnabled = flag;
  }

  /**
   * Sets whether the call to action is shown. For the {@link CallToAction} to be shown, it must be
   * supported as well.
   *
   * @param action {@link CallToAction} you want to display (if it's supported).
   * @param listener Listener to notify when the call to action is clicked.
   * @param position The position in the adapter of the call to action.
   */
  public void setCallToAction(@CallToAction int action, Listener listener, int position) {
    mCallToAction = action;
    mPosition = position;

    Drawable drawable;
    int description;
    OnClickListener onClickListener;
    if (action == CALL_AND_SHARE) {
      drawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_phone_attach);
      drawable.setAutoMirrored(true);
      description = R.string.description_search_call_and_share;
      onClickListener = v -> listener.onCallAndShareIconClicked(position);
    } else if (action == VIDEO && mSupportVideoCall) {
      drawable =
          ContextCompat.getDrawable(getContext(), R.drawable.quantum_ic_videocam_vd_theme_24);
      drawable.setAutoMirrored(true);
      description = R.string.description_search_video_call;
      onClickListener = v -> listener.onVideoCallIconClicked(position);
    } else if (action == DUO) {
      CallIntentBuilder.increaseLightbringerCallButtonAppearInSearchCount();
      drawable =
          ContextCompat.getDrawable(getContext(), R.drawable.quantum_ic_videocam_vd_theme_24);
      drawable.setAutoMirrored(true);
      description = R.string.description_search_video_call;
      onClickListener = v -> listener.onDuoVideoIconClicked(position);
    } else {
      mCallToActionView.setVisibility(View.GONE);
      mCallToActionView.setOnClickListener(null);
      return;
    }

    mCallToActionView.setContentDescription(getContext().getString(description));
    mCallToActionView.setOnClickListener(onClickListener);
    mCallToActionView.setImageDrawable(drawable);
    mCallToActionView.setVisibility(View.VISIBLE);
  }

  public @CallToAction int getCallToAction() {
    return mCallToAction;
  }

  public int getPosition() {
    return mPosition;
  }

  /**
   * Sets whether the view supports a video calling icon. This is independent of whether the view is
   * actually showing an icon. Support for the video calling icon ensures that the layout leaves
   * space for the video icon, should it be shown.
   *
   * @param supportVideoCall {@code true} if the video call icon is supported, {@code false}
   *     otherwise.
   */
  public void setSupportVideoCallIcon(boolean supportVideoCall) {
    mSupportVideoCall = supportVideoCall;
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // We will match parent's width and wrap content vertically, but make sure
    // height is no less than listPreferredItemHeight.
    final int specWidth = resolveSize(0, widthMeasureSpec);
    final int preferredHeight = mPreferredHeight;

    mNameTextViewHeight = 0;
    mPhoneticNameTextViewHeight = 0;
    mLabelViewHeight = 0;
    mDataViewHeight = 0;
    mLabelAndDataViewMaxHeight = 0;
    mSnippetTextViewHeight = 0;
    mStatusTextViewHeight = 0;
    mCheckBoxWidth = 0;

    ensurePhotoViewSize();

    // Width each TextView is able to use.
    int effectiveWidth;
    // All the other Views will honor the photo, so available width for them may be shrunk.
    if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
      effectiveWidth =
          specWidth
              - getPaddingLeft()
              - getPaddingRight()
              - (mPhotoViewWidth + mGapBetweenImageAndText);
    } else {
      effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
    }

    if (mIsSectionHeaderEnabled) {
      effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText;
    }

    effectiveWidth -= (mCallToActionSize + mCallToActionMargin);

    // Go over all visible text views and measure actual width of each of them.
    // Also calculate their heights to get the total height for this entire view.

    if (isVisible(mNameTextView)) {
      // Calculate width for name text - this parallels similar measurement in onLayout.
      int nameTextWidth = effectiveWidth;
      if (mPhotoPosition != PhotoPosition.LEFT) {
        nameTextWidth -= mTextIndent;
      }
      mNameTextView.measure(
          MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
      mNameTextViewHeight = mNameTextView.getMeasuredHeight();
    }

    // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
    // we should ellipsize both using appropriate ratio.
    final int dataWidth;
    final int labelWidth;
    if (isVisible(mDataView)) {
      if (isVisible(mLabelView)) {
        final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
        dataWidth =
            ((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
        labelWidth =
            ((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
      } else {
        dataWidth = effectiveWidth;
        labelWidth = 0;
      }
    } else {
      dataWidth = 0;
      if (isVisible(mLabelView)) {
        labelWidth = effectiveWidth;
      } else {
        labelWidth = 0;
      }
    }

    if (isVisible(mDataView)) {
      mDataView.measure(
          MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
      mDataViewHeight = mDataView.getMeasuredHeight();
    }

    if (isVisible(mLabelView)) {
      mLabelView.measure(
          MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
      mLabelViewHeight = mLabelView.getMeasuredHeight();
    }
    mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);

    if (isVisible(mSnippetView)) {
      mSnippetView.measure(
          MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
      mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
    }

    // Status view height is the biggest of the text view and the presence icon
    if (isVisible(mPresenceIcon)) {
      mPresenceIcon.measure(
          MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
      mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
    }

    mCallToActionView.measure(
        MeasureSpec.makeMeasureSpec(mCallToActionSize, MeasureSpec.EXACTLY),
        MeasureSpec.makeMeasureSpec(mCallToActionSize, MeasureSpec.EXACTLY));

    if (isVisible(mWorkProfileIcon)) {
      mWorkProfileIcon.measure(
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
      mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
    }

    if (isVisible(mStatusView)) {
      // Presence and status are in a same row, so status will be affected by icon size.
      final int statusWidth;
      if (isVisible(mPresenceIcon)) {
        statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin);
      } else {
        statusWidth = effectiveWidth;
      }
      mStatusView.measure(
          MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
      mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
    }

    // Calculate height including padding.
    int height =
        (mNameTextViewHeight
            + mPhoneticNameTextViewHeight
            + mLabelAndDataViewMaxHeight
            + mSnippetTextViewHeight
            + mStatusTextViewHeight);

    // Make sure the height is at least as high as the photo
    height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());

    // Make sure height is at least the preferred height
    height = Math.max(height, preferredHeight);

    // Measure the header if it is visible.
    if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) {
      mHeaderTextView.measure(
          MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    }

    setMeasuredDimension(specWidth, height);
  }

  @Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    final int height = bottom - top;
    final int width = right - left;

    // Determine the vertical bounds by laying out the header first.
    int topBound = 0;
    int leftBound = getPaddingLeft();
    int rightBound = width - getPaddingRight();

    final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);

    // Put the section header on the left side of the contact view.
    if (mIsSectionHeaderEnabled) {
      // Align the text view all the way left, to be consistent with Contacts.
      if (isLayoutRtl) {
        rightBound = width;
      } else {
        leftBound = 0;
      }
      if (mHeaderTextView != null) {
        int headerHeight = mHeaderTextView.getMeasuredHeight();
        int headerTopBound = (height + topBound - headerHeight) / 2 + mTextOffsetTop;

        mHeaderTextView.layout(
            isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
            headerTopBound,
            isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
            headerTopBound + headerHeight);
      }
      if (isLayoutRtl) {
        rightBound -= mHeaderWidth;
      } else {
        leftBound += mHeaderWidth;
      }
    }

    mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, height);
    mLeftOffset = left + leftBound;
    mRightOffset = left + rightBound;
    if (mIsSectionHeaderEnabled) {
      if (isLayoutRtl) {
        rightBound -= mGapBetweenImageAndText;
      } else {
        leftBound += mGapBetweenImageAndText;
      }
    }

    if (mActivatedStateSupported && isActivated()) {
      mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
    }

    final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
    if (mPhotoPosition == PhotoPosition.LEFT) {
      // Photo is the left most view. All the other Views should on the right of the photo.
      if (photoView != null) {
        // Center the photo vertically
        final int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
        photoView.layout(
            leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight);
        leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
      } else if (mKeepHorizontalPaddingForPhotoView) {
        // Draw nothing but keep the padding.
        leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
      }
    } else {
      // Photo is the right most view. Right bound should be adjusted that way.
      if (photoView != null) {
        // Center the photo vertically
        final int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
        photoView.layout(
            rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight);
        rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
      } else if (mKeepHorizontalPaddingForPhotoView) {
        // Draw nothing but keep the padding.
        rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
      }

      // Add indent between left-most padding and texts.
      leftBound += mTextIndent;
    }

    // Place the call to action at the end of the list (e.g. take into account RTL mode).
    // Center the icon vertically
    final int callToActionTop = topBound + (height - topBound - mCallToActionSize) / 2;

    if (!isLayoutRtl) {
      // When photo is on left, icon is placed on the right edge.
      mCallToActionView.layout(
          rightBound - mCallToActionSize,
          callToActionTop,
          rightBound,
          callToActionTop + mCallToActionSize);
    } else {
      // When photo is on right, icon is placed on the left edge.
      mCallToActionView.layout(
          leftBound,
          callToActionTop,
          leftBound + mCallToActionSize,
          callToActionTop + mCallToActionSize);
    }

    if (mPhotoPosition == PhotoPosition.LEFT) {
      rightBound -= (mCallToActionSize + mCallToActionMargin);
    } else {
      leftBound += mCallToActionSize + mCallToActionMargin;
    }

    // Center text vertically, then apply the top offset.
    final int totalTextHeight =
        mNameTextViewHeight
            + mPhoneticNameTextViewHeight
            + mLabelAndDataViewMaxHeight
            + mSnippetTextViewHeight
            + mStatusTextViewHeight;
    int textTopBound = (height + topBound - totalTextHeight) / 2 + mTextOffsetTop;

    // Work Profile icon align top
    int workProfileIconWidth = 0;
    if (isVisible(mWorkProfileIcon)) {
      workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
      final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
      if (mPhotoPosition == PhotoPosition.LEFT) {
        // When photo is on left, label is placed on the right edge of the list item.
        mWorkProfileIcon.layout(
            rightBound - workProfileIconWidth - distanceFromEnd,
            textTopBound,
            rightBound - distanceFromEnd,
            textTopBound + mNameTextViewHeight);
      } else {
        // When photo is on right, label is placed on the left of data view.
        mWorkProfileIcon.layout(
            leftBound + distanceFromEnd,
            textTopBound,
            leftBound + workProfileIconWidth + distanceFromEnd,
            textTopBound + mNameTextViewHeight);
      }
    }

    // Layout all text view and presence icon
    // Put name TextView first
    if (isVisible(mNameTextView)) {
      final int distanceFromEnd =
          workProfileIconWidth
              + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
      if (mPhotoPosition == PhotoPosition.LEFT) {
        mNameTextView.layout(
            leftBound,
            textTopBound,
            rightBound - distanceFromEnd,
            textTopBound + mNameTextViewHeight);
      } else {
        mNameTextView.layout(
            leftBound + distanceFromEnd,
            textTopBound,
            rightBound,
            textTopBound + mNameTextViewHeight);
      }
    }

    if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
      textTopBound += mNameTextViewHeight;
    }

    // Presence and status
    if (isLayoutRtl) {
      int statusRightBound = rightBound;
      if (isVisible(mPresenceIcon)) {
        int iconWidth = mPresenceIcon.getMeasuredWidth();
        mPresenceIcon.layout(
            rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
        statusRightBound -= (iconWidth + mPresenceIconMargin);
      }

      if (isVisible(mStatusView)) {
        mStatusView.layout(
            leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight);
      }
    } else {
      int statusLeftBound = leftBound;
      if (isVisible(mPresenceIcon)) {
        int iconWidth = mPresenceIcon.getMeasuredWidth();
        mPresenceIcon.layout(
            leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight);
        statusLeftBound += (iconWidth + mPresenceIconMargin);
      }

      if (isVisible(mStatusView)) {
        mStatusView.layout(
            statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
      }
    }

    if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
      textTopBound += mStatusTextViewHeight;
    }

    // Rest of text views
    int dataLeftBound = leftBound;

    // Label and Data align bottom.
    if (isVisible(mLabelView)) {
      if (!isLayoutRtl) {
        mLabelView.layout(
            dataLeftBound,
            textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
            rightBound,
            textTopBound + mLabelAndDataViewMaxHeight);
        dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
      } else {
        dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
        mLabelView.layout(
            rightBound - mLabelView.getMeasuredWidth(),
            textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
            rightBound,
            textTopBound + mLabelAndDataViewMaxHeight);
        rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
      }
    }

    if (isVisible(mDataView)) {
      if (!isLayoutRtl) {
        mDataView.layout(
            dataLeftBound,
            textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
            rightBound,
            textTopBound + mLabelAndDataViewMaxHeight);
      } else {
        mDataView.layout(
            rightBound - mDataView.getMeasuredWidth(),
            textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
            rightBound,
            textTopBound + mLabelAndDataViewMaxHeight);
      }
    }
    if (isVisible(mLabelView) || isVisible(mDataView)) {
      textTopBound += mLabelAndDataViewMaxHeight;
    }

    if (isVisible(mSnippetView)) {
      mSnippetView.layout(
          leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight);
    }
  }

  @Override
  public void adjustListItemSelectionBounds(Rect bounds) {
    if (mAdjustSelectionBoundsEnabled) {
      bounds.top += mBoundsWithoutHeader.top;
      bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
      bounds.left = mBoundsWithoutHeader.left;
      bounds.right = mBoundsWithoutHeader.right;
    }
  }

  protected boolean isVisible(View view) {
    return view != null && view.getVisibility() == View.VISIBLE;
  }

  /** Extracts width and height from the style */
  private void ensurePhotoViewSize() {
    if (!mPhotoViewWidthAndHeightAreReady) {
      mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
      if (!mQuickContactEnabled && mPhotoView == null) {
        if (!mKeepHorizontalPaddingForPhotoView) {
          mPhotoViewWidth = 0;
        }
        if (!mKeepVerticalPaddingForPhotoView) {
          mPhotoViewHeight = 0;
        }
      }

      mPhotoViewWidthAndHeightAreReady = true;
    }
  }

  protected int getDefaultPhotoViewSize() {
    return mDefaultPhotoViewSize;
  }

  /**
   * Gets a LayoutParam that corresponds to the default photo size.
   *
   * @return A new LayoutParam.
   */
  private LayoutParams getDefaultPhotoLayoutParams() {
    LayoutParams params = generateDefaultLayoutParams();
    params.width = getDefaultPhotoViewSize();
    params.height = params.width;
    return params;
  }

  @Override
  protected void drawableStateChanged() {
    super.drawableStateChanged();
    if (mActivatedStateSupported) {
      mActivatedBackgroundDrawable.setState(getDrawableState());
    }
  }

  @Override
  protected boolean verifyDrawable(Drawable who) {
    return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
  }

  @Override
  public void jumpDrawablesToCurrentState() {
    super.jumpDrawablesToCurrentState();
    if (mActivatedStateSupported) {
      mActivatedBackgroundDrawable.jumpToCurrentState();
    }
  }

  @Override
  public void dispatchDraw(Canvas canvas) {
    if (mActivatedStateSupported && isActivated()) {
      mActivatedBackgroundDrawable.draw(canvas);
    }

    super.dispatchDraw(canvas);
  }

  /** Sets section header or makes it invisible if the title is null. */
  public void setSectionHeader(String title) {
    if (!TextUtils.isEmpty(title)) {
      if (mHeaderTextView == null) {
        mHeaderTextView = new TextView(getContext());
        mHeaderTextView.setTextAppearance(R.style.SectionHeaderStyle);
        mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
        addView(mHeaderTextView);
      }
      setMarqueeText(mHeaderTextView, title);
      mHeaderTextView.setVisibility(View.VISIBLE);
      mHeaderTextView.setAllCaps(true);
    } else if (mHeaderTextView != null) {
      mHeaderTextView.setVisibility(View.GONE);
    }
  }

  public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
    mIsSectionHeaderEnabled = isSectionHeaderEnabled;
  }

  /** Returns the quick contact badge, creating it if necessary. */
  public QuickContactBadge getQuickContact() {
    if (!mQuickContactEnabled) {
      throw new IllegalStateException("QuickContact is disabled for this view");
    }
    if (mQuickContact == null) {
      mQuickContact = new QuickContactBadge(getContext());
      mQuickContact.setOverlay(null);
      mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
      if (mNameTextView != null) {
        mQuickContact.setContentDescription(
            getContext()
                .getString(R.string.description_quick_contact_for, mNameTextView.getText()));
      }

      addView(mQuickContact);
      mPhotoViewWidthAndHeightAreReady = false;
    }
    return mQuickContact;
  }

  /** Returns the photo view, creating it if necessary. */
  public ImageView getPhotoView() {
    if (mPhotoView == null) {
      mPhotoView = new ImageView(getContext());
      mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
      // Quick contact style used above will set a background - remove it
      mPhotoView.setBackground(null);
      addView(mPhotoView);
      mPhotoViewWidthAndHeightAreReady = false;
    }
    return mPhotoView;
  }

  /** Removes the photo view. */
  public void removePhotoView() {
    removePhotoView(false, true);
  }

  /**
   * Removes the photo view.
   *
   * @param keepHorizontalPadding True means data on the right side will have padding on left,
   *     pretending there is still a photo view.
   * @param keepVerticalPadding True means the View will have some height enough for accommodating a
   *     photo view.
   */
  public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
    mPhotoViewWidthAndHeightAreReady = false;
    mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
    mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
    if (mPhotoView != null) {
      removeView(mPhotoView);
      mPhotoView = null;
    }
    if (mQuickContact != null) {
      removeView(mQuickContact);
      mQuickContact = null;
    }
  }

  /**
   * Sets a word prefix that will be highlighted if encountered in fields like name and search
   * snippet. This will disable the mask highlighting for names.
   *
   * <p>NOTE: must be all upper-case
   */
  public void setHighlightedPrefix(String upperCasePrefix) {
    mHighlightedPrefix = upperCasePrefix;
  }

  /** Clears previously set highlight sequences for the view. */
  public void clearHighlightSequences() {
    mNameHighlightSequence.clear();
    mNumberHighlightSequence.clear();
    mHighlightedPrefix = null;
  }

  /**
   * Adds a highlight sequence to the name highlighter.
   *
   * @param start The start position of the highlight sequence.
   * @param end The end position of the highlight sequence.
   */
  public void addNameHighlightSequence(int start, int end) {
    mNameHighlightSequence.add(new HighlightSequence(start, end));
  }

  /**
   * Adds a highlight sequence to the number highlighter.
   *
   * @param start The start position of the highlight sequence.
   * @param end The end position of the highlight sequence.
   */
  public void addNumberHighlightSequence(int start, int end) {
    mNumberHighlightSequence.add(new HighlightSequence(start, end));
  }

  /** Returns the text view for the contact name, creating it if necessary. */
  public TextView getNameTextView() {
    if (mNameTextView == null) {
      mNameTextView = new TextView(getContext());
      mNameTextView.setSingleLine(true);
      mNameTextView.setEllipsize(getTextEllipsis());
      mNameTextView.setTextColor(mNameTextViewTextColor);
      mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
      // Manually call setActivated() since this view may be added after the first
      // setActivated() call toward this whole item view.
      mNameTextView.setActivated(isActivated());
      mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
      mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
      mNameTextView.setId(R.id.cliv_name_textview);
      mNameTextView.setElegantTextHeight(false);
      addView(mNameTextView);
    }
    return mNameTextView;
  }

  /** Adds or updates a text view for the data label. */
  public void setLabel(CharSequence text) {
    if (TextUtils.isEmpty(text)) {
      if (mLabelView != null) {
        mLabelView.setVisibility(View.GONE);
      }
    } else {
      getLabelView();
      setMarqueeText(mLabelView, text);
      mLabelView.setVisibility(VISIBLE);
    }
  }

  /** Returns the text view for the data label, creating it if necessary. */
  public TextView getLabelView() {
    if (mLabelView == null) {
      mLabelView = new TextView(getContext());
      mLabelView.setLayoutParams(
          new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

      mLabelView.setSingleLine(true);
      mLabelView.setEllipsize(getTextEllipsis());
      mLabelView.setTextAppearance(R.style.TextAppearanceSmall);
      if (mPhotoPosition == PhotoPosition.LEFT) {
        mLabelView.setAllCaps(true);
      } else {
        mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
      }
      mLabelView.setActivated(isActivated());
      mLabelView.setId(R.id.cliv_label_textview);
      addView(mLabelView);
    }
    return mLabelView;
  }

  /**
   * Sets phone number for a list item. This takes care of number highlighting if the highlight mask
   * exists.
   */
  public void setPhoneNumber(String text) {
    mPhoneNumber = text;
    if (text == null) {
      if (mDataView != null) {
        mDataView.setVisibility(View.GONE);
      }
    } else {
      getDataView();

      // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
      // mDataView. Make sure that determination of the highlight sequences are done only
      // after number formatting.

      // Sets phone number texts for display after highlighting it, if applicable.
      // CharSequence textToSet = text;
      final SpannableString textToSet = new SpannableString(text);

      if (mNumberHighlightSequence.size() != 0) {
        final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
        mTextHighlighter.applyMaskingHighlight(
            textToSet, highlightSequence.start, highlightSequence.end);
      }

      setMarqueeText(mDataView, textToSet);
      mDataView.setVisibility(VISIBLE);

      // We have a phone number as "mDataView" so make it always LTR and VIEW_START
      mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
      mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
    }
  }

  public String getPhoneNumber() {
    return mPhoneNumber;
  }

  private void setMarqueeText(TextView textView, CharSequence text) {
    if (getTextEllipsis() == TruncateAt.MARQUEE) {
      // To show MARQUEE correctly (with END effect during non-active state), we need
      // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
      final SpannableString spannable = new SpannableString(text);
      spannable.setSpan(
          TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
      textView.setText(spannable);
    } else {
      textView.setText(text);
    }
  }

  /** Returns the text view for the data text, creating it if necessary. */
  public TextView getDataView() {
    if (mDataView == null) {
      mDataView = new TextView(getContext());
      mDataView.setSingleLine(true);
      mDataView.setEllipsize(getTextEllipsis());
      mDataView.setTextAppearance(R.style.TextAppearanceSmall);
      mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
      mDataView.setActivated(isActivated());
      mDataView.setId(R.id.cliv_data_view);
      mDataView.setElegantTextHeight(false);
      addView(mDataView);
    }
    return mDataView;
  }

  /** Adds or updates a text view for the search snippet. */
  public void setSnippet(String text) {
    if (TextUtils.isEmpty(text)) {
      if (mSnippetView != null) {
        mSnippetView.setVisibility(View.GONE);
      }
    } else {
      mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
      mSnippetView.setVisibility(VISIBLE);
      if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
        // Give the text-to-speech engine a hint that it's a phone number
        mSnippetView.setContentDescription(PhoneNumberUtils.createTtsSpannable(text));
      } else {
        mSnippetView.setContentDescription(null);
      }
    }
  }

  /** Returns the text view for the search snippet, creating it if necessary. */
  public TextView getSnippetView() {
    if (mSnippetView == null) {
      mSnippetView = new TextView(getContext());
      mSnippetView.setSingleLine(true);
      mSnippetView.setEllipsize(getTextEllipsis());
      mSnippetView.setTextAppearance(android.R.style.TextAppearance_Small);
      mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
      mSnippetView.setActivated(isActivated());
      addView(mSnippetView);
    }
    return mSnippetView;
  }

  /** Returns the text view for the status, creating it if necessary. */
  public TextView getStatusView() {
    if (mStatusView == null) {
      mStatusView = new TextView(getContext());
      mStatusView.setSingleLine(true);
      mStatusView.setEllipsize(getTextEllipsis());
      mStatusView.setTextAppearance(android.R.style.TextAppearance_Small);
      mStatusView.setTextColor(mSecondaryTextColor);
      mStatusView.setActivated(isActivated());
      mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
      addView(mStatusView);
    }
    return mStatusView;
  }

  /** Adds or updates a text view for the status. */
  public void setStatus(CharSequence text) {
    if (TextUtils.isEmpty(text)) {
      if (mStatusView != null) {
        mStatusView.setVisibility(View.GONE);
      }
    } else {
      getStatusView();
      setMarqueeText(mStatusView, text);
      mStatusView.setVisibility(VISIBLE);
    }
  }

  /** Adds or updates the presence icon view. */
  public void setPresence(Drawable icon) {
    if (icon != null) {
      if (mPresenceIcon == null) {
        mPresenceIcon = new ImageView(getContext());
        addView(mPresenceIcon);
      }
      mPresenceIcon.setImageDrawable(icon);
      mPresenceIcon.setScaleType(ScaleType.CENTER);
      mPresenceIcon.setVisibility(View.VISIBLE);
    } else {
      if (mPresenceIcon != null) {
        mPresenceIcon.setVisibility(View.GONE);
      }
    }
  }

  /**
   * Set to display work profile icon or not
   *
   * @param enabled set to display work profile icon or not
   */
  public void setWorkProfileIconEnabled(boolean enabled) {
    if (mWorkProfileIcon != null) {
      mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
    } else if (enabled) {
      mWorkProfileIcon = new ImageView(getContext());
      addView(mWorkProfileIcon);
      mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
      mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
      mWorkProfileIcon.setVisibility(View.VISIBLE);
    }
  }

  private TruncateAt getTextEllipsis() {
    return TruncateAt.MARQUEE;
  }

  public void showDisplayName(Cursor cursor, int nameColumnIndex) {
    CharSequence name = cursor.getString(nameColumnIndex);
    setDisplayName(name);

    // Since the quick contact content description is derived from the display name and there is
    // no guarantee that when the quick contact is initialized the display name is already set,
    // do it here too.
    if (mQuickContact != null) {
      mQuickContact.setContentDescription(
          getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText()));
    }
  }

  public void setDisplayName(CharSequence name) {
    if (!TextUtils.isEmpty(name)) {
      // Chooses the available highlighting method for highlighting.
      if (mHighlightedPrefix != null) {
        name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
      } else if (mNameHighlightSequence.size() != 0) {
        final SpannableString spannableName = new SpannableString(name);
        for (HighlightSequence highlightSequence : mNameHighlightSequence) {
          mTextHighlighter.applyMaskingHighlight(
              spannableName, highlightSequence.start, highlightSequence.end);
        }
        name = spannableName;
      }
    } else {
      name = mUnknownNameText;
    }
    setMarqueeText(getNameTextView(), name);

    if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
      // Give the text-to-speech engine a hint that it's a phone number
      mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
      mNameTextView.setContentDescription(PhoneNumberUtils.createTtsSpannable(name.toString()));
    } else {
      // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
      // of the name into two separate parts.
      mNameTextView.setContentDescription(name.toString());
    }
  }

  public void hideDisplayName() {
    if (mNameTextView != null) {
      removeView(mNameTextView);
      mNameTextView = null;
    }
  }

  /** Sets the proper icon (star or presence or nothing) and/or status message. */
  public void showPresenceAndStatusMessage(
      Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) {
    Drawable icon = null;
    int presence = 0;
    if (!cursor.isNull(presenceColumnIndex)) {
      presence = cursor.getInt(presenceColumnIndex);
      icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
    }
    setPresence(icon);

    String statusMessage = null;
    if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
      statusMessage = cursor.getString(contactStatusColumnIndex);
    }
    // If there is no status message from the contact, but there was a presence value, then use
    // the default status message string
    if (statusMessage == null && presence != 0) {
      statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
    }
    setStatus(statusMessage);
  }

  /** Shows search snippet. */
  public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
    if (cursor.getColumnCount() <= summarySnippetColumnIndex
        || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
      setSnippet(null);
      return;
    }

    String snippet = cursor.getString(summarySnippetColumnIndex);

    // Do client side snippeting if provider didn't do it
    final Bundle extras = cursor.getExtras();
    if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {

      final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);

      String displayName = null;
      int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
      if (displayNameIndex >= 0) {
        displayName = cursor.getString(displayNameIndex);
      }

      snippet = updateSnippet(snippet, query, displayName);

    } else {
      if (snippet != null) {
        int from = 0;
        int to = snippet.length();
        int start = snippet.indexOf(SNIPPET_START_MATCH);
        if (start == -1) {
          snippet = null;
        } else {
          int firstNl = snippet.lastIndexOf('\n', start);
          if (firstNl != -1) {
            from = firstNl + 1;
          }
          int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
          if (end != -1) {
            int lastNl = snippet.indexOf('\n', end);
            if (lastNl != -1) {
              to = lastNl;
            }
          }

          StringBuilder sb = new StringBuilder();
          for (int i = from; i < to; i++) {
            char c = snippet.charAt(i);
            if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
              sb.append(c);
            }
          }
          snippet = sb.toString();
        }
      }
    }

    setSnippet(snippet);
  }

  /**
   * Used for deferred snippets from the database. The contents come back as large strings which
   * need to be extracted for display.
   *
   * @param snippet The snippet from the database.
   * @param query The search query substring.
   * @param displayName The contact display name.
   * @return The proper snippet to display.
   */
  private String updateSnippet(String snippet, String query, String displayName) {

    if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
      return null;
    }
    query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());

    // If the display name already contains the query term, return empty - snippets should
    // not be needed in that case.
    if (!TextUtils.isEmpty(displayName)) {
      final String lowerDisplayName = displayName.toLowerCase();
      final List<String> nameTokens = split(lowerDisplayName);
      for (String nameToken : nameTokens) {
        if (nameToken.startsWith(query)) {
          return null;
        }
      }
    }

    // The snippet may contain multiple data lines.
    // Show the first line that matches the query.
    final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);

    if (matched != null && matched.line != null) {
      // Tokenize for long strings since the match may be at the end of it.
      // Skip this part for short strings since the whole string will be displayed.
      // Most contact strings are short so the snippetize method will be called infrequently.
      final int lengthThreshold =
          getResources().getInteger(R.integer.snippet_length_before_tokenize);
      if (matched.line.length() > lengthThreshold) {
        return snippetize(matched.line, matched.startIndex, lengthThreshold);
      } else {
        return matched.line;
      }
    }

    // No match found.
    return null;
  }

  private String snippetize(String line, int matchIndex, int maxLength) {
    // Show up to maxLength characters. But we only show full tokens so show the last full token
    // up to maxLength characters. So as many starting tokens as possible before trying ending
    // tokens.
    int remainingLength = maxLength;
    int tempRemainingLength = remainingLength;

    // Start the end token after the matched query.
    int index = matchIndex;
    int endTokenIndex = index;

    // Find the match token first.
    while (index < line.length()) {
      if (!Character.isLetterOrDigit(line.charAt(index))) {
        endTokenIndex = index;
        remainingLength = tempRemainingLength;
        break;
      }
      tempRemainingLength--;
      index++;
    }

    // Find as much content before the match.
    index = matchIndex - 1;
    tempRemainingLength = remainingLength;
    int startTokenIndex = matchIndex;
    while (index > -1 && tempRemainingLength > 0) {
      if (!Character.isLetterOrDigit(line.charAt(index))) {
        startTokenIndex = index;
        remainingLength = tempRemainingLength;
      }
      tempRemainingLength--;
      index--;
    }

    index = endTokenIndex;
    tempRemainingLength = remainingLength;
    // Find remaining content at after match.
    while (index < line.length() && tempRemainingLength > 0) {
      if (!Character.isLetterOrDigit(line.charAt(index))) {
        endTokenIndex = index;
      }
      tempRemainingLength--;
      index++;
    }
    // Append ellipse if there is content before or after.
    final StringBuilder sb = new StringBuilder();
    if (startTokenIndex > 0) {
      sb.append("...");
    }
    sb.append(line.substring(startTokenIndex, endTokenIndex));
    if (endTokenIndex < line.length()) {
      sb.append("...");
    }
    return sb.toString();
  }

  public void setActivatedStateSupported(boolean flag) {
    this.mActivatedStateSupported = flag;
  }

  public void setAdjustSelectionBoundsEnabled(boolean enabled) {
    mAdjustSelectionBoundsEnabled = enabled;
  }

  @Override
  public void requestLayout() {
    // We will assume that once measured this will not need to resize
    // itself, so there is no need to pass the layout request to the parent
    // view (ListView).
    forceLayout();
  }

  /**
   * Set drawable resources directly for the drawable resource of the photo view.
   *
   * @param drawable A drawable resource.
   */
  public void setDrawable(Drawable drawable) {
    ImageView photo = getPhotoView();
    photo.setScaleType(ImageView.ScaleType.CENTER);
    int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
    photo.setImageDrawable(drawable);
    photo.setImageTintList(ColorStateList.valueOf(iconColor));
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    // If the touch event's coordinates are not within the view's header, then delegate
    // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
    // and ignore the touch event.
    if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
      return super.onTouchEvent(event);
    } else {
      return true;
    }
  }

  private boolean pointIsInView(float localX, float localY) {
    return localX >= mLeftOffset
        && localX < mRightOffset
        && localY >= 0
        && localY < (getBottom() - getTop());
  }

  /**
   * Where to put contact photo. This affects the other Views' layout or look-and-feel.
   *
   * <p>TODO: replace enum with int constants
   */
  public enum PhotoPosition {
    LEFT,
    RIGHT
  }

  protected static class HighlightSequence {

    private final int start;
    private final int end;

    HighlightSequence(int start, int end) {
      this.start = start;
      this.end = end;
    }
  }
}
